# The reverse proxy: TLS, the one public entry point, and the headers that
# belong to the whole site. Checked by `make proxy-check` (caddy fmt and
# caddy validate) and exercised by `make smoke-proxy`.
# Caddy 2.11.4: https://caddyserver.com/docs/caddyfile
{
	# No admin API: config changes need a restart, and 2.11.3 fixed an
	# admin authorisation bypass. Nothing in the container needs it.
	admin off
	# The local CA's root is never installed into the image's trust store
	# (the filesystem is read-only).
	skip_install_trust
	# Unprivileged ports inside the container; publish 80 and 443 to them.
	http_port 8080
	https_port 8443
	servers {
		timeouts {
			read_header 10s
			idle 2m
		}
		max_header_size 16KiB
		# No HTTP/3: its Alt-Svc header would advertise the container's
		# port, 8443, instead of the published 443.
		protocols h1 h2
	}
}

(security_headers) {
	header {
		-Server
		-Via
		# These belong to the site as a whole and are set here, once: an
		# upstream's copy is replaced, not repeated. Content-Security-Policy
		# and the rest are set by the application that renders the page.
{%- if cookiecutter.frontend_nextjs == "yes" %}
		# Permissions-Policy matches frontend/config/security.ts, which
		# sets it when the frontend runs without the proxy.
{%- endif %}
		Strict-Transport-Security "max-age=63072000; includeSubDomains"
		Permissions-Policy "camera=(), microphone=(), geolocation=()"
		Cross-Origin-Opener-Policy "same-origin"
		Cross-Origin-Resource-Policy "same-origin"
	}
}

{$SITE_ADDRESS:localhost} {
	log {
		output stdout
		format json
	}
	import security_headers
	# Mozilla's intermediate profile: Caddy's default cipher suites, with
	# the versions stated so a change of default cannot widen them.
	tls {
		protocols tls1.2 tls1.3
	}
	# Caddy replaces X-Forwarded-For, -Proto and -Host with its own, so the
	# backend can trust them. It passes Forwarded through, so remove it,
	# and x-middleware-subrequest (CVE-2025-29927) never reaches Next.
	request_header -Forwarded
	request_header -X-Middleware-Subrequest
	# A body larger than this is refused with 413 as the upstream reads it.
	# Django refuses form bodies over 2.5 MB itself; this covers anything
	# that streams a body. Raise it here if the API accepts files.
	request_body {
		max_size 10MB
	}
{%- if cookiecutter.frontend_nextjs == "yes" %}

	@backend path /api/* /admin/* /static/* /health/*
	handle @backend {
		reverse_proxy backend:8000 {
			transport http {
				dial_timeout 3s
				response_header_timeout 60s
			}
			# Unbuffered, so streamed responses reach the client as written.
			flush_interval -1
		}
	}
	handle {
		reverse_proxy frontend:3000 {
			transport http {
				dial_timeout 3s
			}
			# App Router pages stream; pass each chunk on as it arrives.
			flush_interval -1
		}
	}
{%- else %}

	reverse_proxy backend:8000 {
		transport http {
			dial_timeout 3s
			response_header_timeout 60s
		}
		# Unbuffered, so streamed responses reach the client as written.
		flush_interval -1
	}
{%- endif %}

	# Caddy's own error pages carry the same headers.
	handle_errors {
		import security_headers
		respond "{err.status_code} {err.status_text}"
	}
}

# The container's health check and metrics, on loopback only.
http://127.0.0.1:2020 {
	bind 127.0.0.1
	respond /healthz 200
	metrics /metrics
}

# Any other host name is refused: the connection is closed unanswered.
http://, https:// {
	abort
}
